雖然昨天已經完成了基本的散步功能,但要讓它真正能在日常使用,還有一個很重要的功能沒有完成,也就是要能夠在背景定位。
因為大部分人都不會把手機一直停留在 App 畫面,可能會鎖屏、切換到其他 App,甚至開走後就不會拿出來看。
背景定位本身並不是什麼困難的技術,真正需要花時間思考的是整條資料流該如何設計。
目前定位流程如下:
進入頁面
↓
Component 執行 watchPosition
↓
呼叫 LocationService 開始定位
↓
更新 React State
↓
收到 GPS 點位
↓
重新渲染地圖路線
前面幾天已經把定位操作整理到 LocationService,定位仍然是由 UI 啟動與停止的。
但實際散步時,使用者不會一直看著 App。他們可能會鎖定螢幕、切換到其他 App,或暫時離開散步畫面。即使畫面不在前景,這次散步仍然還沒有結束,GPS 也應該繼續記錄。
所以流程應該是這樣的:
LocationManager
↓
啟動平台背景定位
↓
收到 GPS 點位
↓
GPS 點位過濾
↓
SQLite 儲存點位
↓
通知 React Component 更新畫面
各自負責的工作如下:
class LocationManager {
async startTracking() {
// 啟動背景定位
}
async pauseTracking() {
// 暫停記錄點位
}
async resumeTracking() {
// 恢復定位
}
async stopTracking() {
// 停止背景定位
}
async handleLocation(location: Location) {
// 過濾 GPS 點
// 寫入 SQLite
// 通知目前畫面更新
}
}
畫面本身只需要訂閱最新的位置資料:
useEffect(() => {
const unsubscribe = LocationManager.subscribe(location => {
setLatestLocation(location);
});
return unsubscribe;
}, []);
這樣定位會跟著散步是否正在進行運作,而不是依賴 UI 是否仍然存在。
另一個很重要的部分就是權限問題。
背景定位不像相機或一般定位只詢問一次權限即可,Android 和 iOS 都建議依照不同階段逐步請求權限,而不是一開始全部一起跳出,不然有些使用者可能會覺得為什麼要請求那麼多權限?
只有確認背景定位權限已經授權後,才允許使用者開始散步。
不同權限狀態也需要對應不同處理方式。
雖然只是一些流程控制,但這些細節會直接影響使用者的體驗。如果沒有規劃好,很容易讓使用者以為 App 無法正常運作。
Android 對背景執行的限制比 iOS 更嚴格一點,即使已經取得背景定位權限,也不代表 App 可以持續在背景接收 GPS 更新。
如果要在Android背景執行定位,就必須透過 Foreground Service,並在通知列顯示一則持續通知。
像下面這樣:

除了定位權限之外,也需要宣告 Foreground Service 相關權限。
android/app/src/main/AndroidManifest.xml 上方需要加上:
// 基本定位需要的權限
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
// 背景定位需要的權限
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
Android 對 Foreground Service 的限制越來越嚴格,因此建議一開始就完整設定,避免後續升級 Android SDK 時才發現服務無法正常啟動。
Android 的背景定位需要透過 Foreground Service 持續執行,因此我另外建立了一個 TraceWalkLocationService,專門負責讓定位工作能在 Android 背景繼續運作。
開始散步時,App 會:
之所以要限制第四點和第五點是因為更新太頻繁會耗電,如果一直在原地打轉的話路線就會非常詭異,所以一定要做限制。
暫停散步時,不用停止 TraceWalkLocationService,而是只停止 GPS 更新,並修改通知顯示的內容。
原因是 Foreground Service 一旦停止,Android 對背景程式的保護也會一起消失。而我的APP有「暫停」的功能,如果使用者去上廁所、買個飲料的話就可以暫停不繼續定位,等到使用者按下「繼續」時,只要重新開始定位,不需要重新建立整個背景服務。
直到真正結束散步,才會停止定位、關閉 TraceWalkLocationService,並移除通知。
和 Android 相比,iOS 不需要 Foreground Service,也不會要求 App 在通知列持續顯示一則通知。
但如果沒有按照官方建議的方式設定,定位很容易在鎖屏或背景後停止更新。
除了在 Xcode 的 Signing & Capabilities 中開啟 Background Modes → Location updates,還需要在 Info.plist 加入定位權限說明。前者是告訴 iOS 允許 App 在背景持續取得位置,後者則是在系統詢問定位權限時,向使用者說明 App 為什麼需要存取位置資訊。
NSLocationAlwaysAndWhenInUseUsageDescription
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>TraceWalk 需要在散步期間持續取得位置,即使螢幕鎖定或 App 位於背景。</string>
這樣不論是使用者或審核人員,都能理解背景定位存在的必要性。
定位功能我使用的是 @react-native-community/geolocation 套件,在初始化時需要先設定背景定位相關選項:
import Geolocation, {
type GeolocationError,
type GeolocationResponse,
} from '@react-native-community/geolocation';
Geolocation.setRNConfiguration({
authorizationLevel: 'always',
enableBackgroundLocationUpdates: true, // 允許 App 在背景持續接收位置更新
});
這兩個屬性都得填寫正確,如果缺少其中一項,都可能導致鎖屏後定位停止更新。